都9102了,该实践 Docker 部署啦

最近用 Docker 完成了 eggjs 后端项目的部署,不得不感叹,Docker 真的是太好用了。不仅能够一键安装 mysql,省去了很多搭环境的事宜,而且可以直接把项目发布到 Docker 容器上进行测试,等项目需要正式上线时,就直接把做好的 Docker 镜像部署上去就好了,省去了很多项目部署上线的风险

Docker 是什么

Docker 是一个可以用来快速部署的轻量级虚拟技术,允许开发人员将自己的程序和运行环境一起打包,制作成一个 DockerImage (镜像),然后部署到服务器上,通过下载这个 Image 就可以将程序跑起来,省去了每次都安装各种依赖和环境的麻烦

Docker 镜像

操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)

Docker 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体

仓库(Docker Registry)

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务

Docker 部署应用有什么优点

使用 Docker 容器部署应用快速方便,特别是应用较多时部署迁移等使用 Docker 会更方便。另外,在同一台服务器上不能同时运行多个 eggjs 应用,除非停止另外一个 eggjs 应用

更详细的介绍可以参考非官方 Docker 中文文档

Docker 的架构

为了后续更好的理解 Docker 命令的操作,我们先来大致理清下 Docker 的架构

image

上面这张图大致介绍了 Docker 的架构,中间是 host,也就是进行 Docker 操作的宿主机,宿主机上主要是运行 Docker Daemon 的核心程序,也就是负责做各种各样的操作,比如说下载 Docker 的镜像,比如说运行一个容器

那宿主机如何和 Docker Daemon 交互呢?实际上是通过在客户端用命令比如 buildrunput 交给 DaemonDaemon 来做实际的操作

右边的蓝的是互联网的 Sass 服务,叫做 registryDaemon 可以和 registry 交互,比如说 push 一个 Image,拖拉一个 Image,实际上是所有 Docker 用户共享 Docker 镜像的服务

image

简单来说,就是客户端和守护进程 Daemon 进行操作,把命令送给守护进程,守护进程来拖取镜像,运行容器,和远端的镜像仓库进行交互

项目部署实践

我这里已经用 eggjs 开发了一个后端项目,然后需要构建一个镜像,然后基于这个 Image 运行一个 container。从而快速实现部署

大体流程

  • 服务器安装好 Docker
  • 本地应用根目录编写好 Dockerfile 文件
  • 将整个应用一起上传到服务器目录下
  • 使用终端连接服务器执行命令构建 Docker Image
  • 基于镜像运行 container,部署成功

具体操作如下

创建 Dockerfile

如果不知道 Dockerfile 文件怎么写,可以直接到 github 上查找 eggjs / docker,就可以看到完整的 Dockerfile 文件,直接拷贝粘贴到项目路径下即可

# 拉取要创建的新镜像的 base image(基础镜像),类似于面向对象里边的基础类
FROM node:8.11.3-alpine

# 设置时区
ENV TIME_ZONE=Asia/Shanghai

# 在容器内运行命令
RUN \
  mkdir -p /usr/src/app \
  && apk add --no-cache tzdata \
  && echo "${TIME_ZONE}" > /etc/timezone \ 
  && ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime 

# 创建 docker 工作目录
WORKDIR /usr/src/app

# 拷贝,把本机当前目录下的 package.json 拷贝到 Image 的 /usr/src/app/ 文件夹下
COPY package.json /usr/src/app/

# 使用 npm 安装 app 所需要的所有依赖
RUN npm i

RUN npm i --registry=https://registry.npm.taobao.org

# 拷贝本地的所有文件到路径中去
COPY . /usr/src/app

# 暴露端口。如果程序是一个服务器,会监听一个或多个端口,可以用 EXPOSE 来表示这个端口
EXPOSE 7001

# 给容器指定一个执行入口
CMD npm run start

注意事项

同时,我们还可以在 eggjs 的官网看到应用部署的文档,有两点需要防止踩坑的地方

image

  • 第一点,因为我们的 Docker 已经是后台了,所以在部署的时候需要去掉 --daemon,不能运行在后台的后台
  • 第二点,我们需要加上 --port=7001,因为 Docker 里有环境变量 PORT,如果不加上,会默认使用 Docker 里的环境变量,而这个变量 PORT 的值是随机生成的数字,所以在正式部署的时候就会开启这个随机数生成的端口,从而报错

image

以上修改都是在 package.json 文件中

构建 Image

Dockerfile 创建完成后,我们就可以在 Dockerfile 文件所在的目录下运行下面的 Docker 命令来构建一个 Image

docker build -t webshare-backend .

通过 -t 的参数,给它一个标签 share,然后给出一个 .. 就是路径名 就是把这个路径底下的所有文件都送给 Docker Engine 让它来产生 Image

image

运行完最后会出现 successfully,代表构建成功

接着我们就可以通过 Docker Images 来查看是否真的生成了这个文件

image

的确是生成了一个新的 Image, 打了一个 taglatest,有一个 ImageId 和大小 size

接着就可以运行这个 Image

运行镜像

docker run -d -p 7001:7001 webshare-backend
  • -d 是 Demon 守护进程,代表容器会在后台运行
  • -p 7001:7001 是把端口暴露出来,p 是做端口映射的,右边的是程序本身的端口,左边是本地的 host 的端口,把本机的 7001 映射到 container 的7001,这样外网就能通过本机的 7001 访问我们的 web 了

image

返回了一个容器的 id,这样就成功的用 Dockerfile 的方式来构建了一个自己的 Image

通过运行 docker ps 可以查看容器是否启动成功

我们还可以用 curl localhost:7001 测试一下,会输出接口查询内容

上传镜像

可以通过 docker push 命令,把自己创建的镜像上传到仓库中来共享

  • 通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
docker tag webshare-backend:latest 服务器ip/webshare-backend:1.0
  • 然后,登录镜像仓库
docker login -u 账号 -p 密码 服务器IP地址

后面显示 Login Succeeded,就是登录成功了

这儿可能第一次会报 Service Unavailable,需要去根路径的 .docker 目录下的 daemon.json 里添加信任

  • 登录成功后,就可以上传自己的镜像到 Docker 仓库
docker push 服务器IP/目录名/webshare-backend:1.0

更新应用

  • 通过 docker ps 命令列出运行的容器
  • docker stop xxx (CONTAINER ID ) 停止运行该容器
  • docker stop $(docker ps -a -q) 暂停所有运行的容器
  • docker rm xxxx 删除container
  • docker rmi 9a52e8ccdd7a(image id) 删除image
  • 提示无法删除的情况下,强制删除,docker rmi -f imageId
  • 按照上面的步骤重新构建镜像和启动容器

其他常用命令

  • 查看镜像构建工程,docker history webshare-backend
  • 列出所有的容器,docker ps -a
  • 查看具体的容器日志,docker logs xxx (CONTAINER ID )
  • docker cp src/. mycontainer:/targethostcontainer 之间拷贝文件

镜像分层

Dockerfile 中的每一行都产生一个新层,存在 Image里的层是只读的(RO)

Image 被运行成为一个容器的时候,就会会产生一个新层,叫容器层 container layer,是可读可写的(RW)

分层的好处:如果有很多的容器和 Image,比如 A Image 有 10 层, B Image 有 7 层,他们之间可能有 5 层是共享的,那么无形之中,存储压力就会小很多

Volume( 数据卷)

提供独立于容器之外的持久化存储

因为在容器中的改动是不会被保存的,Volume 提供了比较方便的、可以持久化存储的一个技巧,比如说运行一个数据库容器,数据库的真正数据应该是被持久化的,Volume 是可以实现的,并且还可以提供给容器之间的共享数据